﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;

namespace Xceed.Silverlight.Data
{
  public sealed class FilterExpressionPreProcessor : FilterExpressionVisitor
  {
    #region Constructor

    public FilterExpressionPreProcessor(
      FilterExpressionBase expression,
      DataSourceProvider dataSourceProvider )
      : base( expression )
    {
      if( dataSourceProvider == null )
        throw new ArgumentNullException( "dataSourceProvider" );

      m_getItemMemberType = dataSourceProvider.GetElementMemberType;
    }

    internal FilterExpressionPreProcessor(
      FilterExpressionBase expression,
      Func<string, Type> getItemMemberType )
      : base( expression )
    {
      if( getItemMemberType == null )
        throw new ArgumentNullException( "getItemMemberType" );

      m_getItemMemberType = getItemMemberType;
    }

    #endregion

    public FilterExpressionPreProcessorResult GetResult()
    {
      this.Visit();

      if( ( m_result != null ) && ( m_result.Target == PreProcessResult.TargetSome ) )
      {
        FilterExpressionBase expression = m_result.Expression;

        // Simplify the FilterExpression as most as possible.
        if( expression != null )
        {
          FilterExpressionSimplifier simplifier = new FilterExpressionSimplifier( expression );
          expression = simplifier.GetResult();

          m_result = new FilterExpressionPreProcessorResult( expression );
        }
      }

      return m_result;
    }

    protected override void Initialize()
    {
      m_result = null;

      base.Initialize();
    }

    protected internal override void Visit( FilterExpression expression )
    {
      object value = expression.Value;

      if( value != null )
      {
        Type memberType = this.GetItemMemberType( expression.MemberName );
        Type valueType;
        value = FilterExpressionPreProcessor.GetConvertedValue( value, memberType, out valueType );

        FilterExpressionPreProcessorResult result = FilterExpressionPreProcessor.GetResult( expression, expression.FilterOperator, value );
        Debug.Assert( ( result.Expression != null ) && ( result.Expression is FilterExpression ) );

        expression = ( FilterExpression )result.Expression;

        // If the filter expression is time-insensitive, we must transformed it into a
        // time sensitive filter expression before processing it.
        if( ( typeof( DateTime ).IsAssignableFrom( value.GetType() ) )
          && ( !expression.IsTimeSensitive.GetValueOrDefault( true ) ) )
        {
          FilterExpressionBase newExpression = FilterExpressionPreProcessor.TransformTimeInsensitiveFilterExpression( expression );

          // Process the new time-sensitive expression.
          this.Visit( newExpression );
          return;
        }
      }

      m_result = this.Expand( expression );
    }

    protected internal override void Visit( AndFilterExpression expression )
    {
      List<FilterExpressionBase> operands = new List<FilterExpressionBase>();

      foreach( FilterExpressionBase childExpression in expression.ChildExpressions )
      {
        this.Visit( childExpression );

        // Retrieve the result of the current operand.
        FilterExpressionPreProcessorResult operand = m_result;

        // An operand that returns no item makes the and-expression totally useless.
        if( operand.Target == PreProcessResult.TargetNone )
          return;

        // An operand that returns all items shouldn't be consider since it doesn't bring
        // anything new to the and-expression.
        if( operand.Expression != null )
        {
          operands.Add( operand.Expression );
        }
      }

      int count = operands.Count;

      // The expression hasn't changed at all.
      if( ( expression.ChildExpressions.Count == count )
        && ( expression.ChildExpressions.SequenceEqual( operands ) ) )
      {
        m_result = new FilterExpressionPreProcessorResult( expression );
      }
      // The and-expression returns all items.
      else if( count == 0 )
      {
        m_result = FilterExpressionPreProcessorResult.TargetAll;
      }
      // The and-expression has only one operand.
      else if( count == 1 )
      {
        m_result = new FilterExpressionPreProcessorResult( operands[ 0 ] );
      }
      else
      {
        m_result = new FilterExpressionPreProcessorResult( new AndFilterExpression( operands ) );
      }
    }

    protected internal override void Visit( OrFilterExpression expression )
    {
      List<FilterExpressionBase> operands = new List<FilterExpressionBase>();

      foreach( FilterExpressionBase childExpression in expression.ChildExpressions )
      {
        this.Visit( childExpression );

        // Retrieve the result of the current operand.
        FilterExpressionPreProcessorResult operand = m_result;

        // An operand that returns all items makes the or-expression totally useless.
        if( operand.Target == PreProcessResult.TargetAll )
          return;

        // An operand that returns no item shouldn't be consider since it doesn't bring
        // anything new to the or-expression.
        if( operand.Expression != null )
        {
          operands.Add( operand.Expression );
        }
      }

      int count = operands.Count;

      // The expression hasn't changed at all.
      if( ( expression.ChildExpressions.Count == count )
        && ( expression.ChildExpressions.SequenceEqual( operands ) ) )
      {
        m_result = new FilterExpressionPreProcessorResult( expression );
      }
      // The or-expression returns no item.
      else if( count == 0 )
      {
        m_result = FilterExpressionPreProcessorResult.TargetNone;
      }
      // The or-expression has only one operand.
      else if( count == 1 )
      {
        m_result = new FilterExpressionPreProcessorResult( operands[ 0 ] );
      }
      else
      {
        m_result = new FilterExpressionPreProcessorResult( new OrFilterExpression( operands ) );
      }
    }

    protected internal override void Visit( NotFilterExpression expression )
    {
      this.Visit( expression.ChildExpression );

      // Retrieve the result of the visited nodes.
      FilterExpressionPreProcessorResult result = m_result;
      FilterExpressionBase childExpression = result.Expression;

      // The expression hasn't changed at all.
      if( childExpression == expression.ChildExpression )
      {
        m_result = new FilterExpressionPreProcessorResult( expression );
      }
      else if( childExpression != null )
      {
        m_result = new FilterExpressionPreProcessorResult( new NotFilterExpression( childExpression ) );
      }
    }

    private static object GetConvertedValue( object value, Type targetType, out Type valueType )
    {
      if( value != null )
      {
        valueType = targetType;

        // Get the real type under the Nullable<>.
        if( valueType.IsNullableStruct() )
        {
          valueType = Nullable.GetUnderlyingType( valueType );
        }

        // Converting to type object is useless.
        if( valueType != typeof( object ) )
        {
          Type type = value.GetType();

          // Try to convert the value only if an implicit cast isn't available.
          if( !valueType.IsAssignableFrom( type ) )
          {
            try
            {
              value = Convert.ChangeType( value, valueType, null );
            }
            catch( Exception )
            {
              // The exception is swallowed intentionally.
              // The conversion failed.
              valueType = type;
            }
          }
        }
      }
      else
      {
        valueType = typeof( object );
      }

      return value;
    }

    private static FilterExpressionBase TransformTimeInsensitiveFilterExpression( FilterExpression expression )
    {
      // Note: In all case, we must return a FilterExpression that is time-sensitive to avoid reentrancy.
      Debug.Assert( expression.Value is DateTime );

      // Remove the time part from the date value.
      DateTime value = ( DateTime )expression.Value;
      value = new DateTime( value.Year, value.Month, value.Day );

      string memberName = expression.MemberName;

      // When dealing with DateTime value with IsTimeSensitive set to false,
      // we must potentialy manipulate the FilterExpression so that the Time
      // part is excluded.
      switch( expression.FilterOperator )
      {
        case FilterOperator.Equal:
          {
            // In the case of "Equal", the date must be greater or equal to the date and less than the date + 1 day.
            return new AndFilterExpression(
              new FilterExpression( memberName, FilterOperator.GreaterThanOrEqual, value, true ),
              new FilterExpression( memberName, FilterOperator.LessThan, value.AddDays( 1d ), true ) );
          }

        case FilterOperator.NotEqual:
          {
            // In the case of "NotEqual", the date must be less than the date and greater or equal to the date + 1 day.
            return new OrFilterExpression(
              new FilterExpression( memberName, FilterOperator.LessThan, value, true ),
              new FilterExpression( memberName, FilterOperator.GreaterThanOrEqual, value.AddDays( 1d ), true ) );
          }

        case FilterOperator.GreaterThan:
          {
            // In the case of "GreaterThan", the date must be greater or equal than the date + 1 day.
            return new FilterExpression( memberName, FilterOperator.GreaterThanOrEqual, value.AddDays( 1d ), true );
          }

        case FilterOperator.LessThanOrEqual:
          {
            // In the case of "LessThanOrEqual", the date must be less than the date + 1 day.
            return new FilterExpression( memberName, FilterOperator.LessThan, value.AddDays( 1d ), true );
          }

        default:
          {
            return new FilterExpression( memberName, expression.FilterOperator, value, true );
          }
      }
    }

    private static FilterExpressionPreProcessorResult GetResult(
      FilterExpression expression,
      FilterOperator filterOperator,
      object value )
    {
      FilterExpression newExpression = expression;

      if( ( expression.FilterOperator != filterOperator ) || ( expression.Value != value ) )
      {
        newExpression = ( FilterExpression )expression.Clone();
        newExpression.FilterOperator = filterOperator;
        newExpression.Value = value;
      }

      return new FilterExpressionPreProcessorResult( newExpression );
    }

    private FilterExpressionPreProcessorResult Expand( FilterExpression expression )
    {
      //
      // Here is an example of a valid expression:
      //
      // item.ShipCity > value
      // 
      // Since ShipCity may be "null", some comparison operators may not work
      // on some providers.  We will consider the "null" value as the lowest
      // possible value.  In order to get the desired items, some expressions
      // need to be converted to another expression.
      //
      // If the value parameter is "null", here are the resulting expressions:
      // 
      // * item.ShipCity > value   =>  item.ShipCity != null
      // * item.ShipCity >= value  =>  (no filtering needed)
      // * item.ShipCity < value   =>  (filter all)
      // * item.ShipCity <= value  =>  item.ShipCity == null
      //
      // If the value parameter is not "null", here are the resulting expressions:
      //
      // * item.ShipCity > value   =>  item.ShipCity > value
      // * item.ShipCity >= value  =>  item.ShipCity >= value
      // * item.ShipCity < value   =>  item.ShipCity == null || item.ShipCity < value
      // * item.ShipCity <= value  =>  item.ShipCity == null || item.ShipCity <= value

      object value = expression.Value;

      if( value == null )
        return this.HandleNullValue( expression );

      Type valueType = value.GetType();

      if( typeof( bool ).IsAssignableFrom( valueType ) )
        return this.HandleBooleanValue( expression );

      if( typeof( IComparable ).IsAssignableFrom( valueType ) )
        return this.HandleComparableValue( expression );

      if( ( expression.FilterOperator == FilterOperator.Equal )
        || ( expression.FilterOperator == FilterOperator.NotEqual ) )
        return this.HandleEquatableValue( expression );

      throw new InvalidOperationException( "An attempt was made to use an unsupported filter expression." );
    }

    private FilterExpressionPreProcessorResult HandleNullValue( FilterExpression expression )
    {
      switch( expression.FilterOperator )
      {
        case FilterOperator.Equal:
        case FilterOperator.LessThanOrEqual:
          {
            Type memberType = this.GetItemMemberType( expression.MemberName );

            // Make sure the member type can be compare to null.
            if( memberType.IsNullableType() )
              return this.CompareEqualNull( expression );

            return FilterExpressionPreProcessorResult.TargetNone;
          }

        case FilterOperator.NotEqual:
        case FilterOperator.GreaterThan:
          {
            Type memberType = this.GetItemMemberType( expression.MemberName );

            // Make sure the member type can be compare to null.
            if( memberType.IsNullableType() )
              return this.CompareNotEqualNull( expression );

            return FilterExpressionPreProcessorResult.TargetAll;
          }

        case FilterOperator.GreaterThanOrEqual:
          {
            // All possible values are greater than or equal to null.
            return FilterExpressionPreProcessorResult.TargetAll;
          }

        case FilterOperator.LessThan:
          {
            // No value can be less than null.
            return FilterExpressionPreProcessorResult.TargetNone;
          }

        case FilterOperator.Contains:
        case FilterOperator.ContainedIn:
        case FilterOperator.StartsWith:
        case FilterOperator.EndsWith:
          {
            // No value may starts, ends or contains the null value.
            return FilterExpressionPreProcessorResult.TargetNone;
          }

        default:
          throw new InvalidOperationException( "An attempt was made to use an unsupported filter expression." );
      }
    }

    private FilterExpressionPreProcessorResult HandleBooleanValue( FilterExpression expression )
    {
      bool value = ( bool )expression.Value;

      // Handle the comparison of boolean values.  Here is the order considered
      // for boolean values: null < false < true.
      // 
      switch( expression.FilterOperator )
      {
        case FilterOperator.Equal:
          {
            return this.CompareEqual( expression, value );
          }

        case FilterOperator.NotEqual:
          {
            return this.CompareNotEqual( expression, value );
          }

        case FilterOperator.GreaterThan:
          {
            // No value is greater than true.
            if( value )
              return FilterExpressionPreProcessorResult.TargetNone;

            return this.CompareEqual( expression, true );
          }

        case FilterOperator.GreaterThanOrEqual:
          {
            // Only true is greater than or equal to true.
            if( value )
              return this.CompareEqual( expression, true );

            Type memberType = this.GetItemMemberType( expression.MemberName );

            // Only the null value is lower than false.
            if( memberType.IsNullableType() )
              return this.CompareNotEqualNull( expression );

            return FilterExpressionPreProcessorResult.TargetAll;
          }

        case FilterOperator.LessThan:
          {
            // Only true is greater than false.
            if( value )
              return this.CompareNotEqual( expression, true );

            Type memberType = this.GetItemMemberType( expression.MemberName );

            // Only the null value is lower than false.
            if( memberType.IsNullableType() )
              return this.CompareEqualNull( expression );

            return FilterExpressionPreProcessorResult.TargetNone;
          }

        case FilterOperator.LessThanOrEqual:
          {
            // No value is greater than true.
            if( value )
              return FilterExpressionPreProcessorResult.TargetAll;

            return this.CompareNotEqual( expression, true );
          }

        default:
          throw new InvalidOperationException( "An attempt was made to use an unsupported filter expression." );
      }
    }

    private FilterExpressionPreProcessorResult HandleComparableValue( FilterExpression expression )
    {
      if( expression.FilterOperator == FilterOperator.Equal )
      {
        return this.CompareEqual( expression, expression.Value );
      }
      else if( expression.FilterOperator == FilterOperator.NotEqual )
      {
        return this.CompareNotEqual( expression, expression.Value );
      }
      else
      {
        return this.Compare( expression );
      }
    }

    private FilterExpressionPreProcessorResult HandleEquatableValue( FilterExpression expression )
    {
      if( expression.FilterOperator == FilterOperator.Equal )
      {
        return this.CompareEqual( expression, expression.Value );
      }
      else if( expression.FilterOperator == FilterOperator.NotEqual )
      {
        return this.CompareNotEqual( expression, expression.Value );
      }

      throw new InvalidOperationException( "An attempt was made to use an unsupported filter expression." );
    }

    private FilterExpressionPreProcessorResult CompareEqualNull( FilterExpression expression )
    {
      return FilterExpressionPreProcessor.GetResult( expression, FilterOperator.Equal, null );
    }

    private FilterExpressionPreProcessorResult CompareNotEqualNull( FilterExpression expression )
    {
      return FilterExpressionPreProcessor.GetResult( expression, FilterOperator.NotEqual, null );
    }

    private FilterExpressionPreProcessorResult CompareEqual( FilterExpression expression, object value )
    {
      if( value == null )
        return this.CompareEqualNull( expression );

      FilterExpressionPreProcessorResult compare = FilterExpressionPreProcessor.GetResult( expression, FilterOperator.Equal, value );
      Type memberType = this.GetItemMemberType( expression.MemberName );

      if( !memberType.IsNullableStruct() )
        return compare;

      // Take care of a nullable member type.
      FilterExpressionPreProcessorResult handleNull = this.CompareNotEqualNull( expression );

      Debug.Assert( ( handleNull.Expression != null )
                 && ( compare.Expression != null ) );

      FilterExpressionBase newExpression = new AndFilterExpression( handleNull.Expression, compare.Expression );

      return new FilterExpressionPreProcessorResult( newExpression );
    }

    private FilterExpressionPreProcessorResult CompareNotEqual( FilterExpression expression, object value )
    {
      if( value == null )
        return this.CompareNotEqualNull( expression );

      FilterExpressionPreProcessorResult compare = FilterExpressionPreProcessor.GetResult( expression, FilterOperator.NotEqual, value );
      Type memberType = this.GetItemMemberType( expression.MemberName );

      if( !memberType.IsNullableStruct() )
        return compare;

      // Take care of a nullable member type.
      FilterExpressionPreProcessorResult handleNull = this.CompareEqualNull( expression );

      Debug.Assert( ( handleNull.Expression != null )
                 && ( compare.Expression != null ) );

      FilterExpressionBase newExpression = new OrFilterExpression( handleNull.Expression, compare.Expression );

      return new FilterExpressionPreProcessorResult( newExpression );
    }

    private FilterExpressionPreProcessorResult Compare( FilterExpression expression )
    {
      FilterExpressionBase newExpression = expression;
      Type memberType = this.GetItemMemberType( expression.MemberName );

      // Take care of a nullable member type.
      if( memberType.IsNullableType() )
      {
        switch( expression.FilterOperator )
        {
          case FilterOperator.GreaterThan:
          case FilterOperator.GreaterThanOrEqual:
            {
              FilterExpressionPreProcessorResult handleNull = this.CompareNotEqualNull( expression );

              newExpression = new AndFilterExpression( handleNull.Expression, newExpression );
            }
            break;

          case FilterOperator.LessThan:
          case FilterOperator.LessThanOrEqual:
            {
              FilterExpressionPreProcessorResult handleNull = this.CompareEqualNull( expression );

              newExpression = new OrFilterExpression( handleNull.Expression, newExpression );
            }
            break;

          case FilterOperator.StartsWith:
          case FilterOperator.EndsWith:
          case FilterOperator.Contains:
          case FilterOperator.ContainedIn:
            {
              if( typeof( string ) == memberType )
              {
                FilterExpressionPreProcessorResult handleNull = this.CompareNotEqualNull( expression );

                newExpression = new AndFilterExpression( handleNull.Expression, newExpression );
              }
            }
            break;

          default:
            break;
        }
      }

      return new FilterExpressionPreProcessorResult( newExpression );
    }

    private Type GetItemMemberType( string propertyName )
    {
      return m_getItemMemberType.Invoke( propertyName );
    }

    #region Private Fields

    private FilterExpressionPreProcessorResult m_result;
    private Func<string, Type> m_getItemMemberType;

    #endregion
  }
}
